P09-2. Grafana Loki와 서비스 메시 로그 아키텍처 - 실습 검증
Part 09 (실습): LGTM 로그 파이프라인 — Grafana Loki와 서비스 메시 로그 아키텍처 검증
monitoring네임스페이스에 클라우드 네이티브 로그 저장소인Grafana Loki(3.4.2) 를 배포하고,Grafana Alloy에 쿠버네티스 파드 로그 수집 파이프라인(loki.source.kubernetes→loki.write)을 연결한다.- Istio
TelemetryAPI 리소스를 적용해 파드 재시작 없이Waypoint Proxy의 L7 Envoy 액세스 로그를 실시간으로 활성화한다. Waypoint가 남긴 L7 HTTP 액세스 로그와 외부 PostgreSQL 18 VM(172.19.0.7:5432)으로 향하는 TCP 액세스 로그의 각 컬럼 구조를 해부한다.Loki의/loki/api/v1/query_rangeAPI에 직접LogQL쿼리를 날려part1-demo의 L7 로그와istio-system(ztunnel)의 L4 HBONE 로그를 초고속으로 검색·검증한다.
- P09-1 이론 개념을 먼저 읽고 오면 왜 Loki가 라벨만 색인하는지, 그리고 Envoy 응답 플래그(
UO,UT,UF)와LogQL문법이 어떻게 동작하는지 명확히 이해됩니다. - Part 08 실습에서 구축한
monitoring네임스페이스(alloy,vmsingle) 위에서 바로 이어서 진행합니다. - Windows PowerShell 창을 열고 아래 명령어로 상태를 확인합니다.
$env:Path += ";$HOME\bin" cd "$HOME\mesh-study" kubectl get pods -n monitoring
실습 아키텍처 및 로그 수집 파이프라인 한눈에 보기
flowchart LR
subgraph LOG_SOURCES["로그 발생원 (Kubernetes Pods)"]
ZT["ztunnel (istio-system)
L4 HBONE 연결 로그 (logfmt)
src.identity / dst.hbone_addr"]
WP["Waypoint (part1-demo)
Telemetry API로 활성화!
L7 Envoy HTTP & TCP 액세스 로그"]
APP["order-api (part1-demo)
애플리케이션 컨테이너 로그"]
end
subgraph ALLOY_LOKI["monitoring 네임스페이스"]
ALLOY["Grafana Alloy (v1.21.2)
1. discovery.relabel (namespace/pod/app)
2. loki.source.kubernetes
3. loki.write"]
LOKI["Grafana Loki (3.4.2)
:3100 (/loki/api/v1/push & /query_range)
라벨 색인 + 본문 고압축 청크 저장"]
end
ZT -->|"Pod Log Stream"| ALLOY
WP -->|"Pod Log Stream"| ALLOY
APP -->|"Pod Log Stream"| ALLOY
ALLOY ==>|"Loki Push"| LOKIStep 1. Grafana Loki (3.4.2) 배포 및 Grafana Alloy 로그 파이프라인 확장
먼저 monitoring 네임스페이스에 loki (grafana/loki:3.4.2) 를 배포하고, alloy-config ConfigMap에 part1-demo와 istio-system 네임스페이스의 파드 로그를 실시간으로 수집해 Loki로 밀어 넣는 loki.source.kubernetes 블록을 추가합니다.
동시에 part1-demo 네임스페이스에 Istio kind: Telemetry 리소스를 적용해 Waypoint Proxy의 Envoy 액세스 로그 출력을 켭니다!
# part9-loki-stack.yaml 생성 및 적용
@"
apiVersion: apps/v1
kind: Deployment
metadata:
name: loki
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app: loki
template:
metadata:
labels:
app: loki
spec:
containers:
- name: loki
image: grafana/loki:3.4.2
args:
- "-config.file=/etc/loki/local-config.yaml"
ports:
- containerPort: 3100
---
apiVersion: v1
kind: Service
metadata:
name: loki
namespace: monitoring
spec:
selector:
app: loki
ports:
- name: http
port: 3100
targetPort: 3100
---
apiVersion: telemetry.istio.io/v1
kind: Telemetry
metadata:
name: mesh-observability
namespace: part1-demo
spec:
accessLogging:
- providers:
- name: envoy
"@ | Set-Content -Path "$HOME\mesh-study\part9-loki-stack.yaml" -Encoding utf8
kubectl apply -f "$HOME\mesh-study\part9-loki-stack.yaml"
kubectl wait --for=condition=Ready pod -l app=loki -n monitoring --timeout=120s
이어서 Grafana Alloy 설정(alloy-config)에 아래 loki.source.kubernetes 파이프라인이 포함되도록 업데이트하고 Alloy를 재시작합니다(Part 10의 Tempo OTLP 설정까지 한 번에 포함된 통합 설정입니다):
// [L] Kubernetes 파드 로그 수집 (part1-demo 및 istio-system) -> Loki 전송
discovery.relabel "mesh_logs" {
targets = discovery.kubernetes.pods.targets
rule {
source_labels = ["__meta_kubernetes_namespace"]
regex = "part1-demo|istio-system"
action = "keep"
}
rule {
source_labels = ["__meta_kubernetes_namespace"]
target_label = "namespace"
}
rule {
source_labels = ["__meta_kubernetes_pod_name"]
target_label = "pod"
}
rule {
source_labels = ["__meta_kubernetes_pod_container_name"]
target_label = "container"
}
rule {
source_labels = ["__meta_kubernetes_pod_label_app"]
target_label = "app"
}
}
loki.source.kubernetes "mesh_logs" {
targets = discovery.relabel.mesh_logs.output
forward_to = [loki.write.loki.receiver]
}
loki.write "loki" {
endpoint {
url = "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/push"
}
}
Step 2. Telemetry API로 활성화된 Waypoint Envoy 액세스 로그 실물 해부!
이제 study-client에서 추적하기 쉬운 고유 요청 번호 헤더(-H "x-request-id: 99999999-aaaa-bbbb-cccc-1234567890ab")를 붙여 order-api를 3번 호출한 뒤, Waypoint Proxy 파드가 출력한 Envoy 액세스 로그를 직접 열어봅니다!
# 1. 고유 x-request-id 헤더를 포함하여 3회 요청 전송
kubectl exec -n part1-demo study-client -- curl -s -H "x-request-id: 99999999-aaaa-bbbb-cccc-1234567890ab" "http://order-api/trace-and-log-check?[1-3]" | Out-Null
# 2. Waypoint Proxy 파드의 실시간 Envoy 액세스 로그 확인!
kubectl logs -n part1-demo -l gateway.networking.k8s.io/gateway-name=waypoint --tail=5
실제 출력 결과:
2026-10-25T07:38:21.161606Z info Envoy proxy is ready
[2026-10-25T07:38:23.338Z] "- - -" 0 - - - "-" 3709 6990 83 - "-" "-" "-" "-" "172.19.0.7:5432" inbound-vip|5432|tcp|pg-vm.mesh.internal 10.244.2.20:36068 240.240.0.50:5432 10.244.2.14:39092 - -
[2026-10-25T07:38:24.430Z] "GET /trace-and-log-check?1 HTTP/1.1" 200 - via_upstream - "-" 0 616 13 10 "-" "curl/8.10.1" "99999999-aaaa-bbbb-cccc-1234567890ab" "order-api" "envoy://connect_originate/10.244.2.6:8080" inbound-vip|80|http|order-api.part1-demo.svc.cluster.local envoy://internal_client_address/ 10.96.72.69:80 10.244.2.7:38104 - default
[2026-10-25T07:38:24.452Z] "GET /trace-and-log-check?2 HTTP/1.1" 200 - via_upstream - "-" 0 616 9 8 "-" "curl/8.10.1" "99999999-aaaa-bbbb-cccc-1234567890ab" "order-api" "envoy://connect_originate/10.244.2.6:8080" inbound-vip|80|http|order-api.part1-demo.svc.cluster.local envoy://internal_client_address/ 10.96.72.69:80 10.244.2.7:38104 - default
[2026-10-25T07:38:24.463Z] "GET /trace-and-log-check?3 HTTP/1.1" 200 - via_upstream - "-" 0 619 10 8 "-" "curl/8.10.1" "99999999-aaaa-bbbb-cccc-1234567890ab" "order-api" "envoy://connect_originate/10.244.2.10:8080" inbound-vip|80|http|order-api.part1-demo.svc.cluster.local envoy://internal_client_address/ 10.96.72.69:80 10.244.2.7:38104 - default
Waypoint 로그에 찍힌 두 종류의 핵심 기록을 해부해 봅시다!1. 첫 번째 줄 (외부 PostgreSQL 18 VM으로 나간 L4 TCP 로그):
"- - -" 0 - ... 3709 6990 83 ... "172.19.0.7:5432" inbound-vip|5432|tcp|pg-vm.mesh.internal ... 240.240.0.50:5432postgres-exporter파드가 가상 IP(240.240.0.50:5432)로 접속해 외부 Linux VM(172.19.0.7:5432)과3,709바이트를 보내고6,990바이트를 받으며83ms동안 통신한 기록이Waypoint에 정확히 남았습니다!
2. 두 번째~네 번째 줄 (order-api로 들어간 L7 HTTP 로그):
"GET /trace-and-log-check?1 HTTP/1.1": HTTP 메서드, 경로, 프로토콜200 - via_upstream: HTTP 상태 코드(200), Envoy 응답 플래그(-= 정상), 응답 출처(via_upstream= 백엔드 파드가 정상 응답함; 만약 서킷 브레이커가 터지면 여기가503 UO upstream_reset_before_response_started{overflow}로 찍힙니다!)0 616 13 10: 받은 본문 크기(0B), 보낸 응답 크기(616B), 전체 소요 시간(13ms), 순수 백엔드 파드 처리 시간(10ms) (즉, 프록시 네트워크 오버헤드는 단3ms!)"99999999-aaaa-bbbb-cccc-1234567890ab": 요청의 고유 추적 ID (x-request-id)"envoy://connect_originate/10.244.2.6:8080":Waypoint가 이 요청을HBONE(CONNECT)터널에 실어10.244.2.6:8080파드로 보냈다는 뜻!
Step 3. Grafana Loki API에 LogQL 쿼리를 날려 실시간 검색 검증!
이제 Grafana Alloy가 위 로그들을 Grafana Loki 저장소(:3100)로 잘 밀어 넣었는지, Loki의 /loki/api/v1/query_range API에 직접 LogQL 쿼리를 날려 확인합니다!
kubectl exec -- curl로 Loki 쿼리를 날릴 때의 팁
PowerShell은 작은따옴표 안의 큰따옴표({namespace="part1-demo"})를 외부 프로그램(kubectl)에 넘길 때 벗겨내는 성질이 있습니다. 따라서 URL에 %22(큰따옴표)로 인코딩하거나 Grafana 화면에서 조회하면 따옴표 유실 없이 100% 정확하게 동작합니다.
# 1. [LogQL 쿼리 1] {namespace="part1-demo"} |= "/trace-and-log-check" (Waypoint L7 로그 검색!)
kubectl exec -n part1-demo study-client -- curl -s -g "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/query_range?query=%7Bnamespace%3D%22part1-demo%22%7D%7C%3D%22/trace-and-log-check%22&limit=2"
# 2. [LogQL 쿼리 2] {namespace="istio-system"} |= "connection complete" (ztunnel L4 HBONE 로그 검색!)
kubectl exec -n part1-demo study-client -- curl -s -g "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/query_range?query=%7Bnamespace%3D%22istio-system%22%7D%7C%3D%22connection%20complete%22&limit=2"
실제 출력 결과 (핵심 JSON 발췌):
// [1. part1-demo의 Waypoint L7 Envoy 액세스 로그 검색 결과 (execTime: 0.18초!)]
{
"status": "success",
"data": {
"resultType": "streams",
"result": [
{
"stream": {
"container": "istio-proxy",
"job": "loki.source.kubernetes.mesh_logs",
"namespace": "part1-demo",
"pod": "waypoint-5c968455-ml6h7",
"service_name": "istio-proxy"
},
"values": [
[
"1792913905356707846",
"[2026-10-25T07:38:24.463Z] \"GET /trace-and-log-check?3 HTTP/1.1\" 200 - via_upstream - \"-\" 0 619 10 8 \"-\" \"curl/8.10.1\" \"99999999-aaaa-bbbb-cccc-1234567890ab\" \"order-api\" \"envoy://connect_originate/10.244.2.10:8080\" ...\n"
]
]
}
]
}
}
// [2. istio-system의 ztunnel L4 HBONE 액세스 로그 검색 결과 (execTime: 0.029초!)]
{
"status": "success",
"data": {
"resultType": "streams",
"result": [
{
"stream": {
"app": "ztunnel",
"container": "istio-proxy",
"job": "loki.source.kubernetes.mesh_logs",
"namespace": "istio-system",
"pod": "ztunnel-zb2gf",
"service_name": "ztunnel"
},
"values": [
[
"1792913993667655162",
"2026-10-25T07:39:53.657461Z\tinfo\taccess\tconnection complete\tsrc.addr=10.244.2.7:38102 src.workload=\"study-client\" src.namespace=\"part1-demo\" src.identity=\"spiffe://cluster.local/ns/part1-demo/sa/default\" ... dst.addr=10.244.2.17:15008 dst.hbone_addr=10.244.2.17:3100 dst.service=\"loki.monitoring.svc.cluster.local\" ...\n"
]
]
}
]
}
}
- 초고속 검색 확인:
stats.summary를 보면 Loki가 초당 1,599만 바이트(15.9MB/s)의 속도로 로그 청크를 스캔하여 단 0.029초(29ms) 만에 원하는 로그 줄을 정확히 뽑아냈습니다!
Step 4. 자주 발생하는 문제와 해결 (Troubleshooting)
| 증상 / 에러 메시지 | 원인 | 해결 방법 |
|---|---|---|
Waypoint 파드 로그(kubectl logs)에 아무런 HTTP 요청 기록이 안 찍힘 |
Istio는 성능 최적화를 위해 기본적으로 Envoy 액세스 로그를 꺼 둠 | Step 1처럼 part1-demo(또는 istio-system 전역)에 kind: Telemetry (accessLogging.providers: [{name: envoy}]) 리소스를 적용 |
PowerShell에서 curl ... 'query={namespace="part1-demo"}' 실행 시 syntax error: unexpected IDENTIFIER, expecting STRING 발생 |
PowerShell이 외부 프로세스 호출 시 문자열 내부의 큰따옴표(")를 제거해 {namespace=part1-demo}로 전달함 |
큰따옴표 자리에 URL 인코딩(%22)을 쓰거나 \"로 이스케이프 처리 |
Alloy 로그에 pods/log is forbidden: User "system:serviceaccount:monitoring:alloy" cannot list resource "pods/log" 에러 |
Alloy 서비스 계정의 ClusterRole에 pods/log 리소스 권한이 누락됨 |
ClusterRole(alloy-observer)의 resources 배열에 "pods/log"를 반드시 포함 |
Step 5. 이번 파트 핵심 명령어 정리 (Command Reference)
# 1. Istio Telemetry 리소스 확인 (액세스 로깅 및 트레이싱 활성화 상태)
kubectl get telemetry -n part1-demo -o yaml
# 2. Waypoint Proxy가 실시간으로 찍는 Envoy 액세스 로그 직접 확인
kubectl logs -n part1-demo -l gateway.networking.k8s.io/gateway-name=waypoint --tail=10
# 3. Grafana Loki에 저장된 로그 라벨(Stream Labels) 종류 확인
kubectl exec -n part1-demo study-client -- curl -s "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/labels"
Step 6. 셀프 체크 (Self-Check Q&A)
Q1. Grafana Loki가 Elasticsearch보다 메모리를 훨씬 적게 먹으면서도 빠르게 동작하는 비결은 무엇이며, 라벨을 설계할 때 무엇을 주의해야 하나요?
Loki는 로그 본문 전체를 단어별로 색인(Full-Text Indexing)하지 않고, 오직 파드의 메타데이터 라벨(namespace, pod, app 등)만 색인한 뒤 본문은 압축해 저장하기 때문입니다. 따라서 라벨에는 값의 종류가 적은(저카디널리티) 속성만 넣어야 하며, request_id나 client_ip처럼 종류가 무한히 늘어나는 고카디널리티(High Cardinality) 값은 라벨에 넣지 말고 쿼리 시점에 파이프(|= "...", | json, | logfmt)로 필터링해야 합니다.
Q2. Envoy(Waypoint) 액세스 로그에서 응답 코드 옆에 찍히는 UO, UT, UF 플래그는 각각 무슨 뜻인가요?
UO(Upstream Overflow):DestinationRule의 커넥션 풀 한도를 초과하여 서킷 브레이커가 작동해 차단됨UT(Upstream Request Timeout): 백엔드 서버가 지정된timeout시간 내에 응답하지 못해 연결을 끊음(504)UF(Upstream Connection Failure): 백엔드 파드나 외부 VM에 TCP 연결 자체를 실패함
🔗 다음 파트로 이동
이제 M(Metric) 과 L(Log) 두 기둥이 모두 세워졌습니다!
다음 Part 10에서는 세 번째 기둥인 T(Grafana Tempo) 를 통해 Waypoint가 자동으로 찍어주는 W3C Trace Context (traceparent) 헤더와 OpenTelemetry(OTLP :4317) 분산 트레이스를 직접 추적해 봅니다!
네비게이션
| 이전 | 목록 | 다음 |
|---|---|---|
| ◀ Part 09 이론 | 목록으로 | Part 10 이론 ▶ |